Select a Date & Time
+Eastern Time - US & Canada ()
+ +diff --git a/README.md b/README.md index 20cee34..daaf3e1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,95 @@ -# freesched +# Quick Start +`npm install` + +`node server.js` + +Browse to http://localhost:3000/admin for admin interface. + +Create a UID and follow link in admin interface for frontend. + +# Introducing FreeSched: A Simpler Alternative to Cal.com +You’ve asked for a much simpler, easy-to-use version of Cal.com, named "FreeSched," that retains the clean and intuitive UI/UX of Cal.com and mirrors the simplicity of Calendly.com. Below, I’ll outline what FreeSched is, its core features, how it works, and why it’s a streamlined alternative to Cal.com—all while keeping the design aesthetic you love. + +What is FreeSched? +FreeSched is a lightweight, open-source scheduling tool designed to make booking meetings as simple as possible. Inspired by Cal.com (an open-source scheduling infrastructure) and Calendly.com (a popular scheduling platform), FreeSched strips away complexity to focus on the essentials. It’s perfect for individuals—like freelancers, consultants, or anyone needing quick one-on-one scheduling—while maintaining a modern, minimalistic interface similar to Cal.com and Calendly. + +Key Features of FreeSched +FreeSched focuses on simplicity without sacrificing usability. Here’s what it offers: + +Single Event Type +Users set one type of event with a fixed duration (e.g., 30 minutes). +No need to juggle multiple event types—just one straightforward option. +Basic Availability Settings +Set your available days and hours using a simple weekly grid (e.g., Monday to Friday, 9 AM to 5 PM). +Availability is divided into 30-minute slots for easy selection. +Google Calendar Integration +Connects to your Google Calendar to check existing events and prevent double-booking. +Only shows time slots when you’re truly free. +Simple Booking Page +A clean, public page (e.g., freesched.example.com/yourname) where others can pick a date and time. +Invitees provide their name and email to confirm the booking. +Email Notifications +Sends confirmation emails to you and the invitee with all the details. +Keeps everyone in the loop without extra effort. +Time Zone Support +Automatically adjusts available times to the invitee’s time zone. +No more confusion for international scheduling. +How FreeSched Works +FreeSched is designed to be intuitive for both the user (you) and the invitee (the person booking with you). Here’s the process: + +For You (The User): +Sign Up and Set Up +Log in with Google (which also links your Google Calendar). +Set your availability (e.g., weekdays from 9 AM to 5 PM) and event duration (e.g., 30 minutes). +Share Your Booking Link +Get a unique URL (e.g., freesched.example.com/yourname) to share via email, social media, or anywhere. +Manage Bookings +View upcoming bookings in a simple dashboard. +Cancel bookings if needed, with automatic notifications sent to both parties. +For Invitees (The Booker): +Visit Your Booking Page +They open your link and see a calendar with your available dates. +Pick a Time +They select a date and choose from your available 30-minute slots, adjusted to their time zone. +Confirm the Booking +They enter their name and email, hit confirm, and receive an email with the details. +Once booked, the event is added to your Google Calendar, and both of you get a confirmation email. That’s it—no fuss, no extra steps. + +UI/UX: Inspired by Cal.com and Calendly +FreeSched keeps the polished, user-friendly design of Cal.com and Calendly: + +Clean Layout: A minimalistic interface with plenty of whitespace, clear fonts, and a simple color scheme. +Intuitive Navigation: Easy-to-use calendar for picking dates and a straightforward list of time slots. +Responsive Design: Works seamlessly on desktops, tablets, and phones. +Booking Flow: Select a date → pick a time → enter details → confirm—all in a few clicks. +Think of Calendly’s booking page or Cal.com’s sleek dashboard, but pared down to the essentials. The focus is on clarity and ease, avoiding overwhelming options or cluttered screens. + +How It’s Simpler Than Cal.com +Cal.com is a powerful scheduling infrastructure with tons of features, but that can make it overkill for simple needs. FreeSched simplifies things: + +Feature Cal.com FreeSched +Event Types Multiple (15-min, 30-min, etc.) One fixed duration (e.g., 30 min) +Availability Detailed rules and buffers Basic weekly grid +Integrations Multiple calendars, payments, etc. Google Calendar only +Team Support Yes (shared schedules, etc.) Individual use only +Customization Highly customizable Simple, uniform design +FreeSched cuts out the extras to deliver a tool that’s fast to set up and easy to use, while still looking and feeling like its more complex cousins. + +Technical Overview (For the Curious) +FreeSched is built with modern, open-source tools: + +Frontend: Next.js (a React framework for fast, server-rendered pages). +Backend: Node.js with Express.js for handling bookings and logic. +Database: PostgreSQL with Prisma to store user data and bookings. +Authentication: NextAuth.js with Google OAuth for login and calendar access. +Emails: SendGrid or Nodemailer for notifications. +This keeps FreeSched lightweight, secure, and easy to maintain, with a tech stack similar to Cal.com but scaled back for simplicity. + +Why Use FreeSched? +FreeSched is for anyone who wants a free, no-nonsense scheduling tool: + +Perfect For: Freelancers, tutors, consultants, or anyone needing simple one-on-one bookings. +Benefits: Quick setup, no learning curve, and a professional look inspired by Cal.com and Calendly. +Open-Source: Like Cal.com, it’s free to use and customizable if you’re a developer. +It’s scheduling made easy—just share a link and let others book you, all wrapped in a familiar, user-friendly package. \ No newline at end of file diff --git a/db/freesched.db b/db/freesched.db new file mode 100644 index 0000000..4f6a4ae Binary files /dev/null and b/db/freesched.db differ diff --git a/db/init.js b/db/init.js new file mode 100644 index 0000000..91c9463 --- /dev/null +++ b/db/init.js @@ -0,0 +1,19 @@ +// db/init.js +const sqlite3 = require('sqlite3').verbose(); +const db = new sqlite3.Database('db/freesched.db'); + +db.serialize(() => { + db.run(`CREATE TABLE IF NOT EXISTS availability ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date TEXT NOT NULL UNIQUE, + times TEXT NOT NULL + )`); + + // Example initial data for April 2021 (matching screenshot) + db.run(`INSERT OR IGNORE INTO availability (date, times) VALUES (?, ?)`, + ['2025-02-21', '9:30am,10:00am,10:30am,11:00am,11:30am,2:00pm']); + db.run(`INSERT OR IGNORE INTO availability (date, times) VALUES (?, ?)`, + ['2025-02-22', '10:00am,11:00am,2:00pm']); +}); + +db.close(); \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..dc2b5d7 --- /dev/null +++ b/favicon.ico @@ -0,0 +1,38 @@ + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5c8fade --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2322 @@ +{ + "name": "freesched-app", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "freesched-app", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "express": "^4.18.2", + "sqlite3": "^5.1.6" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "license": "MIT", + "optional": true + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "license": "MIT", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT", + "optional": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "optional": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC", + "optional": true + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC", + "optional": true + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "license": "ISC", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "optional": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "optional": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "optional": true + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT", + "optional": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "license": "ISC", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.74.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", + "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "license": "MIT", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "license": "ISC", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/socks-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socks-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "optional": true + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "license": "ISC", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..cefc539 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "freesched-app", + "version": "1.0.0", + "description": "FreeSched Meeting Scheduler", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.18.2", + "sqlite3": "^5.1.6" + }, + "keywords": [], + "author": "", + "license": "ISC" +} \ No newline at end of file diff --git a/public/admin.js b/public/admin.js new file mode 100644 index 0000000..d4ef2bc --- /dev/null +++ b/public/admin.js @@ -0,0 +1,826 @@ +// public/admin.js (for admin.html) + +// Global variables +let currentAdminDate = new Date(); +let currentAdminMonth = currentAdminDate.getMonth(); +let currentAdminYear = currentAdminDate.getFullYear(); +let selectedAdminDate = null; +const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + +// Generate all possible time slots in chronological order (8am to 5pm) +const timeSlotsData = [ + '8:00am', '9:00am', '10:00am', '11:00am', '12:00pm', + '1:00pm', '2:00pm', '3:00pm', '4:00pm', '5:00pm' +]; + +// Helper function to convert time string to minutes since midnight +function timeToMinutes(timeStr) { + const match = timeStr.match(/([0-9]+)(?::([0-9]+))?(am|pm)/i); + if (!match) return 0; + + let hours = parseInt(match[1]); + const minutes = parseInt(match[2] || '0'); + const meridian = match[3].toLowerCase(); + + if (meridian === 'pm' && hours !== 12) { + hours += 12; + } else if (meridian === 'am' && hours === 12) { + hours = 0; + } + + return hours * 60 + minutes; +} + +// Helper function to ensure consistent time format +function formatTimeSlot(hour) { + const meridian = hour >= 12 ? 'pm' : 'am'; + const displayHour = hour > 12 ? hour - 12 : (hour === 0 ? 12 : hour); + return `${displayHour}:00${meridian}`; +} + +// Render the admin calendar with available dates highlighted +async function renderAdminCalendar(month, year) { + const adminCalendarDates = document.getElementById('adminCalendarDates'); + const adminMonthYear = document.getElementById('adminMonthYear'); + if (!adminCalendarDates || !adminMonthYear) { + console.error('Required calendar elements not found'); + return; + } + + // Clear existing calendar + adminCalendarDates.innerHTML = ''; + adminMonthYear.textContent = `${months[month]} ${year}`; + + // Get current UID and availability + const selectedUid = document.getElementById('uidSelect').value; + if (!selectedUid) { + // If no UID selected, just show empty calendar + return; + } + + const firstDay = new Date(year, month, 1).getDay(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const today = new Date(); + const availability = await fetchAvailability(); + + // Start of today (midnight) + const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()); + + // Create blank cells for days before the first day of the month + for (let i = 0; i < firstDay; i++) { + const blank = document.createElement('div'); + blank.classList.add('date-item', 'p-2', 'rounded-pill', 'text-center', 'text-muted'); + adminCalendarDates.appendChild(blank); + } + + // Clear any existing selected date + const existingSelected = adminCalendarDates.querySelector('.selected'); + if (existingSelected) { + existingSelected.classList.remove('selected'); + } + + // Populate the calendar with days + let todayElement = null; + for (let i = 1; i <= daysInMonth; i++) { + const day = document.createElement('div'); + day.classList.add('date-item', 'p-2', 'rounded-pill', 'text-center'); + day.textContent = i; + const date = new Date(year, month, i); + const dateStr = date.toISOString().split('T')[0]; + + // Check if date is in the past + const isPastDate = date < startOfToday; + + if (date.toDateString() === today.toDateString()) { + day.classList.add('current'); + todayElement = day; + } + if (availability[dateStr]) { + day.classList.add('available'); + } + if (isPastDate) { + day.classList.add('disabled'); + } else { + day.addEventListener('click', () => selectAdminDate(date, firstDay)); + } + adminCalendarDates.appendChild(day); + } + + // If a date was previously selected, re-select it + if (selectedAdminDate && + selectedAdminDate.getMonth() === month && + selectedAdminDate.getFullYear() === year) { + selectAdminDate(selectedAdminDate, firstDay); + } +} + +// Handle admin date selection +async function selectAdminDate(date, firstDay) { + selectedAdminDate = date; + const calendarDates = document.getElementById('adminCalendarDates'); + if (!calendarDates) return; + + // Remove selected class from all dates + const allDates = calendarDates.querySelectorAll('.date-item'); + allDates.forEach(item => item.classList.remove('selected')); + + // Add selected class to the clicked date + const selectedDay = Array.from(allDates).find(item => { + if (!item.textContent) return false; + const dayText = item.textContent; + const itemDate = new Date(currentAdminYear, currentAdminMonth, parseInt(dayText)); + return itemDate.toDateString() === date.toDateString(); + }); + if (selectedDay) selectedDay.classList.add('selected'); + const selectedDateDisplay = document.getElementById('selectedDateDisplay'); + if (selectedDateDisplay) { + selectedDateDisplay.textContent = `Selected Date: ${date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}`; + } + + // Update time slots for the selected date + await updateTimeSlots(date); +} + +// Update time slots based on selected date +async function updateTimeSlots(date) { + const availability = await fetchAvailability(); + const dateStr = date.toISOString().split('T')[0]; + let availableTimes = []; + + // Handle case where availability might be undefined or null + if (availability && availability[dateStr]) { + if (typeof availability[dateStr] === 'string') { + availableTimes = availability[dateStr].split(',').map(t => t.trim()); + } else if (Array.isArray(availability[dateStr])) { + availableTimes = availability[dateStr].map(t => t.trim()); + } + } + + // Sort available times chronologically + availableTimes.sort((a, b) => timeToMinutes(a) - timeToMinutes(b)); + + const timeSlots = document.getElementById('timeSlots'); + if (!timeSlots) return; + + timeSlots.innerHTML = ''; + + // Get current time in minutes for comparison + const now = new Date(); + const currentMinutes = now.getHours() * 60 + now.getMinutes(); + const isToday = date.toDateString() === now.toDateString(); + + // Create a map of all time slots with their status + const allSlots = new Map(); + timeSlotsData.forEach(time => { + const timeInMinutes = timeToMinutes(time); + // Skip times that are in the past on the current day + if (isToday && timeInMinutes <= currentMinutes) { + return; + } + allSlots.set(time, availableTimes.includes(time)); + }); + + // Sort all slots chronologically + const sortedSlots = Array.from(allSlots.entries()) + .sort((a, b) => timeToMinutes(a[0]) - timeToMinutes(b[0])); + + // Display all slots in chronological order + sortedSlots.forEach(([time, isAvailable]) => { + const button = document.createElement('button'); + button.classList.add('time-slot', 'btn', 'rounded-pill', 'w-100', 'mb-2', 'py-2'); + button.textContent = time; + + if (isAvailable) { + button.classList.add('btn-primary', 'available'); + button.addEventListener('click', () => { + button.classList.remove('btn-primary', 'available'); + button.classList.add('btn-outline-secondary'); + removeTime(dateStr, time); + }); + } else { + button.classList.add('btn-outline-secondary'); + button.addEventListener('click', () => { + button.classList.remove('btn-outline-secondary'); + button.classList.add('btn-primary', 'available'); + addSingleTime(dateStr, time); + }); + } + + timeSlots.appendChild(button); + }); +} + +// Fetch available dates from the API +async function fetchAvailability() { + try { + const selectedUid = document.getElementById('uidSelect').value; + if (!selectedUid) return {}; + + const response = await fetch(`/api/availability/${selectedUid}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const availability = await response.json(); + return availability; + } catch (error) { + console.error('Error fetching availability:', error); + return {}; + } +} + +// Helper function to update calendar UI after availability changes +async function updateAvailabilityUI() { + const availability = await fetchAvailability(); + if (selectedAdminDate) { + await updateTimeSlots(selectedAdminDate); + } + + const calendarDates = document.getElementById('adminCalendarDates'); + if (calendarDates) { + const allDateElements = calendarDates.querySelectorAll('.date-item'); + allDateElements.forEach(dateElement => { + if (!dateElement.textContent) return; // Skip empty cells + const day = parseInt(dateElement.textContent); + const date = new Date(currentAdminYear, currentAdminMonth, day); + const dateStr = date.toISOString().split('T')[0]; + + if (availability[dateStr]) { + dateElement.classList.add('available'); + } else { + dateElement.classList.remove('available'); + } + }); + } +} + +// UID management functions +async function loadUids() { + try { + const response = await fetch('/api/uids'); + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Failed to fetch UIDs'); + } + const uids = await response.json(); + + const select = document.getElementById('uidSelect'); + select.innerHTML = ''; + + uids.forEach(uidObj => { + const option = document.createElement('option'); + option.value = uidObj.uid; + option.textContent = uidObj.uid; + select.appendChild(option); + }); + } catch (error) { + console.error('Error loading UIDs:', error); + } +} + +async function createUid(uid) { + try { + const response = await fetch('/api/uids', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ uid }) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Failed to create UID'); + } + + // Reload UIDs and select the new one + await loadUids(); + + // Select the new UID and trigger change event + const select = document.getElementById('uidSelect'); + select.value = uid; + select.dispatchEvent(new Event('change')); + + + return true; + } catch (error) { + console.error('Error creating UID:', error); + return false; + } +} + +async function deleteUid(uid) { + try { + const response = await fetch(`/api/uids/${uid}`, { + method: 'DELETE' + }); + + if (!response.ok) throw new Error('Failed to delete UID'); + await loadUids(); + return true; + } catch (error) { + console.error('Error deleting UID:', error); + return false; + } +} + + + +// Add a single time for the selected date +async function addSingleTime(dateStr, time) { + const selectedUid = document.getElementById('uidSelect').value; + if (!selectedUid) { + alert('Please select a UID first'); + return; + } + + try { + const availability = await fetchAvailability(); + let times = []; + if (availability && availability[dateStr]) { + if (typeof availability[dateStr] === 'string') { + times = availability[dateStr].split(',').map(t => t.trim()); + } else if (Array.isArray(availability[dateStr])) { + times = [...availability[dateStr]]; + } + } + times.push(time); + + const response = await fetch(`/api/availability/${selectedUid}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ date: dateStr, times: [...new Set(times)] }) // Remove duplicates + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + console.log(result.message); + await updateAvailabilityUI(); + } catch (error) { + console.error('Error adding time slot:', error); + alert('Failed to add time slot. Please try again.'); + } +} + +// Remove a single time for the selected date +async function removeTime(dateStr, time) { + const selectedUid = document.getElementById('uidSelect').value; + if (!selectedUid) { + alert('Please select a UID first'); + return; + } + + try { + const availability = await fetchAvailability(); + let times = []; + if (availability && availability[dateStr]) { + if (typeof availability[dateStr] === 'string') { + times = availability[dateStr].split(',').filter(t => t.trim() !== time); + } else if (Array.isArray(availability[dateStr])) { + times = availability[dateStr].filter(t => t.trim() !== time); + } + } + + const response = await fetch(`/api/availability/${selectedUid}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ date: dateStr, times }) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `HTTP error! status: ${response.status}`); + } + await updateAvailabilityUI(); + } catch (error) { + console.error('Error removing time slot:', error); + alert('Failed to remove time slot. Please try again.'); + } +} + +// Helper function to convert time string to minutes since midnight +function timeToMinutes(timeStr) { + const match = timeStr.match(/([0-9]+)(?::([0-9]+))?(am|pm)/i); + if (!match) return 0; + + let [_, hours, minutes, period] = match; + hours = parseInt(hours); + minutes = minutes ? parseInt(minutes) : 0; + period = period.toLowerCase(); + + if (period === 'pm' && hours !== 12) hours += 12; + if (period === 'am' && hours === 12) hours = 0; + + return hours * 60 + minutes; +} + +// Helper function to ensure consistent time format +function formatTimeSlot(hour) { + const period = hour < 12 ? 'am' : 'pm'; + const displayHour = hour === 0 ? 12 : (hour > 12 ? hour - 12 : hour); + return `${displayHour}:00${period}`; +} + +// Handle admin date selection +async function selectAdminDate(date, firstDay) { + // Check if the date is in the past + const today = new Date(); + const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()); + if (date < startOfToday) { + console.warn('Attempted to select a past date'); + return; + } + + // Update selected date and time slots + selectedAdminDate = date; + await updateTimeSlots(date); + const dateItems = document.querySelectorAll('#adminCalendarDates .date-item'); + dateItems.forEach(item => item.classList.remove('selected')); // Ensure only one date is selected + // Find the exact date item for the clicked date + const selectedDay = Array.from(dateItems).find(item => { + const dayText = item.textContent; + const itemDate = new Date(currentAdminYear, currentAdminMonth, parseInt(dayText)); + return itemDate.toDateString() === date.toDateString(); + }); + if (selectedDay) selectedDay.classList.add('selected'); + const selectedDateDisplay = document.getElementById('selectedDateDisplay'); + if (selectedDateDisplay) { + selectedDateDisplay.textContent = `Selected Date: ${date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}`; + } +} + +// Update time slots based on selected date +async function updateTimeSlots(date) { + + const availability = await fetchAvailability(); + const dateStr = date.toISOString().split('T')[0]; + let availableTimes = []; + + // Handle case where availability might be undefined or null + if (availability && availability[dateStr]) { + if (typeof availability[dateStr] === 'string') { + availableTimes = availability[dateStr].split(',').map(t => t.trim()); + } else if (Array.isArray(availability[dateStr])) { + availableTimes = availability[dateStr].map(t => t.trim()); + } + } + + // Sort available times chronologically + availableTimes.sort((a, b) => timeToMinutes(a) - timeToMinutes(b)); + + const timeSlots = document.getElementById('timeSlots'); + if (!timeSlots) return; + + timeSlots.innerHTML = ''; + + // Get current time in minutes for comparison + const now = new Date(); + const currentMinutes = now.getHours() * 60 + now.getMinutes(); + const isToday = date.toDateString() === now.toDateString(); + + // Create a map of all time slots with their status + const allSlots = new Map(); + timeSlotsData.forEach(time => { + const timeInMinutes = timeToMinutes(time); + // Skip times that are in the past on the current day + if (isToday && timeInMinutes <= currentMinutes) { + return; + } + allSlots.set(time, availableTimes.includes(time)); + }); + + // Sort all slots chronologically + const sortedSlots = Array.from(allSlots.entries()) + .sort((a, b) => timeToMinutes(a[0]) - timeToMinutes(b[0])); + + // Display all slots in chronological order + sortedSlots.forEach(([time, isAvailable]) => { + const button = document.createElement('button'); + button.classList.add('time-slot', 'btn', 'rounded-pill', 'w-100', 'mb-2', 'py-2'); + button.textContent = time; + + if (isAvailable) { + button.classList.add('btn-primary', 'available'); + button.addEventListener('click', () => { + button.classList.remove('btn-primary', 'available'); + button.classList.add('btn-outline-secondary'); + removeTime(dateStr, time); + }); + } else { + button.classList.add('btn-outline-secondary'); + button.addEventListener('click', () => { + button.classList.remove('btn-outline-secondary'); + button.classList.add('btn-primary', 'available'); + addSingleTime(dateStr, time); + }); + } + + timeSlots.appendChild(button); + }); +} + +// Fetch available dates from the API +async function fetchAvailability() { + try { + const selectedUid = document.getElementById('uidSelect').value; + if (!selectedUid) return {}; + + const response = await fetch(`/api/availability/${selectedUid}`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const availability = await response.json(); + return availability; + } catch (error) { + console.error('Error fetching availability:', error); + return {}; + } +} + +// Helper function to update calendar UI after availability changes +async function updateAvailabilityUI() { + const availability = await fetchAvailability(); + if (selectedAdminDate) { + await updateTimeSlots(selectedAdminDate); + } + + const calendarDates = document.getElementById('adminCalendarDates'); + if (calendarDates) { + const allDateElements = calendarDates.querySelectorAll('.date-item'); + allDateElements.forEach(dateElement => { + if (!dateElement.textContent) return; // Skip empty cells + const day = parseInt(dateElement.textContent); + const date = new Date(currentAdminYear, currentAdminMonth, day); + const dateStr = date.toISOString().split('T')[0]; + + if (availability[dateStr]) { + dateElement.classList.add('available'); + } else { + dateElement.classList.remove('available'); + } + }); + } +} + +document.addEventListener('DOMContentLoaded', async () => { + // Load existing UIDs + await loadUids(); + + // Handle UID selection + const uidSelect = document.getElementById('uidSelect'); + const deleteUidBtn = document.getElementById('deleteUid'); + const uidUrl = document.getElementById('uidUrl'); + + uidSelect.addEventListener('change', async () => { + const selectedUid = uidSelect.value; + deleteUidBtn.disabled = !selectedUid; + flushDatabaseBtn.disabled = !selectedUid; + + // Clear current selections + selectedAdminDate = null; + const selectedDateElem = document.getElementById('selectedDateDisplay'); + const timeSlotsElem = document.getElementById('timeSlots'); + const calendarDatesElem = document.getElementById('adminCalendarDates'); + const adminMonthYear = document.getElementById('adminMonthYear'); + + // Clear all UI elements + if (selectedDateElem) selectedDateElem.textContent = ''; + if (timeSlotsElem) timeSlotsElem.innerHTML = ''; + if (calendarDatesElem) calendarDatesElem.innerHTML = ''; + if (adminMonthYear) adminMonthYear.textContent = ''; + + if (selectedUid) { + const url = `${window.location.origin}/${selectedUid}`; + uidUrl.textContent = url; + uidUrl.href = url; + // Refresh calendar with new UID's data + await renderAdminCalendar(currentAdminMonth, currentAdminYear); + // Show availability for current date + if (selectedAdminDate) { + await updateTimeSlots(selectedAdminDate); + } + } else { + uidUrl.textContent = 'Select a UID first'; + uidUrl.href = '#'; + // Add placeholder text + if (selectedDateElem) selectedDateElem.textContent = 'Please select a UID to view the calendar'; + await renderAdminCalendar(currentAdminMonth, currentAdminYear); + } + }); + + // Handle new UID creation + const newUidInput = document.getElementById('newUid'); + const createUidBtn = document.getElementById('createUid'); + + createUidBtn.addEventListener('click', async () => { + const uid = newUidInput.value.trim().toLowerCase(); + + if (!/^[a-z0-9-]+$/.test(uid)) { + alert('UID can only contain lowercase letters, numbers, and hyphens'); + return; + } + + if (await createUid(uid)) { + newUidInput.value = ''; + } else { + alert('Failed to create UID. Please try again.'); + } + }); + + // Handle UID deletion + deleteUidBtn.addEventListener('click', async () => { + const uid = uidSelect.value; + if (!uid) return; + + if (!confirm(`Are you sure you want to delete UID: ${uid}?\nThis will remove all associated availability data.`)) { + return; + } + + if (await deleteUid(uid)) { + // Clear selection and trigger change event + uidSelect.value = ''; + uidSelect.dispatchEvent(new Event('change')); + } else { + alert('Failed to delete UID. Please try again.'); + } + }); + + // Initialize UI elements + const adminCalendarDates = document.getElementById('adminCalendarDates'); + const adminMonthYear = document.getElementById('adminMonthYear'); + const adminPrevMonthBtn = document.getElementById('adminPrevMonth'); + const adminNextMonthBtn = document.getElementById('adminNextMonth'); + const selectedDateDisplay = document.getElementById('selectedDateDisplay'); + const timeSlots = document.getElementById('timeSlots'); + const flushDatabaseBtn = document.getElementById('flushDatabase'); + const resetDatabaseBtn = document.getElementById('resetDatabase'); + + // Handle database reset + resetDatabaseBtn.addEventListener('click', async () => { + if (!confirm('⚠️ WARNING: This will permanently delete ALL UIDs and their associated availability data. This action cannot be undone.\n\nAre you absolutely sure you want to proceed?')) { + return; + } + + // Double confirm for dangerous action + if (!confirm('Please confirm one more time that you want to reset the entire database. All data will be lost.')) { + return; + } + + try { + const response = await fetch('/api/reset', { + method: 'POST' + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Failed to reset database'); + } + + // Reload the page to reset all state + window.location.reload(); + } catch (error) { + console.error('Error resetting database:', error); + alert('Failed to reset database. Please try again.'); + } + }); + + + + + + + + + // Check if a date has availability + async function checkAvailabilityForDate(date) { + const availability = await fetchAvailability(); + const dateStr = date.toISOString().split('T')[0]; + return !!availability[dateStr]; + } + + + + + + // Flush (delete) all entries from the database + flushDatabaseBtn.addEventListener('click', async () => { + const selectedUid = document.getElementById('uidSelect').value; + if (!selectedUid) { + alert('Please select a UID first'); + return; + } + + if (!confirm(`Are you sure you want to delete ALL availability entries for UID: ${selectedUid}?`)) { + return; + } + + try { + console.log(`Attempting to delete all availability for UID: ${selectedUid}...`); + const response = await fetch(`/api/availability/${selectedUid}/flush`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' } + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + console.log('Delete availability response:', result); + + // Refresh data + await fetchAvailability(); // Refresh availability data + + // Reset UI + selectedAdminDate = null; // Clear selected date + selectedDateDisplay.textContent = ''; + timeSlots.innerHTML = ''; // Clear time slots + await renderAdminCalendar(currentAdminMonth, currentAdminYear); + } catch (error) { + console.error('Error deleting availability:', error); + alert('Failed to delete availability. Please try again.'); + } + }); + + // Navigate to previous month (admin) + adminPrevMonthBtn.addEventListener('click', () => { + currentAdminMonth--; + if (currentAdminMonth < 0) { + currentAdminMonth = 11; + currentAdminYear--; + } + renderAdminCalendar(currentAdminMonth, currentAdminYear); + }); + + // Navigate to next month (admin) + adminNextMonthBtn.addEventListener('click', () => { + currentAdminMonth++; + if (currentAdminMonth > 11) { + currentAdminMonth = 0; + currentAdminYear++; + } + renderAdminCalendar(currentAdminMonth, currentAdminYear); + }); + + // Handle global database flush + const flushGlobalDatabaseBtn = document.getElementById('flushGlobalDatabase'); + if (flushGlobalDatabaseBtn) { + flushGlobalDatabaseBtn.addEventListener('click', async () => { + if (!confirm('WARNING: This will delete ALL UIDs and their availability data. This action cannot be undone. Are you sure you want to proceed?')) { + return; + } + + try { + const response = await fetch('/api/flush-global', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' } + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || `HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + console.log('Global database flush response:', result); + alert(result.message || 'Database flushed successfully!'); + + // Reset everything + selectedAdminDate = null; + const selectedDateElem = document.getElementById('selectedDateDisplay'); + const timeSlotsElem = document.getElementById('timeSlots'); + const calendarDatesElem = document.getElementById('adminCalendarDates'); + if (selectedDateElem) selectedDateElem.textContent = ''; + if (timeSlotsElem) timeSlotsElem.innerHTML = ''; + if (calendarDatesElem) calendarDatesElem.innerHTML = ''; + + // Reload UIDs + await loadUids(); + + // Update UID URL display + const uidUrl = document.getElementById('uidUrl'); + if (uidUrl) uidUrl.textContent = 'Select a UID first'; + + // Disable delete button + const deleteUidBtn = document.getElementById('deleteUid'); + if (deleteUidBtn) deleteUidBtn.disabled = true; + } catch (error) { + console.error('Error flushing database:', error); + alert('Failed to flush database. Please try again.'); + } + }); + } + + // Initial render with current date + renderAdminCalendar(currentAdminMonth, currentAdminYear); + if (currentAdminDate.getDate() <= new Date(currentAdminYear, currentAdminMonth, 0).getDate()) { + const hasAvailability = await checkAvailabilityForDate(currentAdminDate); + if (hasAvailability) { + selectAdminDate(currentAdminDate, new Date(currentAdminYear, currentAdminMonth, 1).getDay()); + } + } +}); \ No newline at end of file diff --git a/public/avatar.jpg b/public/avatar.jpg new file mode 100644 index 0000000..62b90be Binary files /dev/null and b/public/avatar.jpg differ diff --git a/public/public.js b/public/public.js new file mode 100644 index 0000000..a266ba6 --- /dev/null +++ b/public/public.js @@ -0,0 +1,425 @@ +// public/public.js (for index.html) + +// Convert time string (e.g., '9:30am' or '2:00pm') to minutes since midnight +function timeToMinutes(timeStr) { + const match = timeStr.match(/([0-9]{1,2}):([0-9]{2})(am|pm)/i); + if (!match) { + console.error('Invalid time format:', timeStr); + return 0; + } + const [_, hours, minutes, period] = match; + let totalHours = parseInt(hours); + if (period.toLowerCase() === 'pm' && totalHours !== 12) totalHours += 12; + if (period.toLowerCase() === 'am' && totalHours === 12) totalHours = 0; + return totalHours * 60 + parseInt(minutes); +} + +// Get UID from URL path +function getUidFromPath() { + const path = window.location.pathname.substring(1); // Remove leading slash + return path || null; +} + +// Validate UID format +function isValidUid(uid) { + return /^[a-z0-9-]+$/.test(uid); +} + +document.addEventListener('DOMContentLoaded', async () => { + const uid = getUidFromPath(); + const mainContent = document.getElementById('mainContent'); + const uidError = document.getElementById('uidError'); + + // Check if we have a valid UID + if (!uid || !isValidUid(uid)) { + mainContent.style.display = 'none'; + uidError.style.display = 'block'; + return; + } + + // Hide error, show main content + uidError.style.display = 'none'; + mainContent.style.display = 'block'; + + const calendarDates = document.getElementById('calendarDates'); + const monthYear = document.getElementById('monthYear'); + const prevMonthBtn = document.getElementById('prevMonth'); + const nextMonthBtn = document.getElementById('nextMonth'); + const currentTimeElement = document.getElementById('currentTime'); + const timeSlotsContainer = document.getElementById('timeSlots'); + + // Check if navigation buttons exist before adding event listeners + if (!prevMonthBtn || !nextMonthBtn) { + console.error('Navigation buttons (prevMonth or nextMonth) not found in the DOM'); + return; + } + + let currentDate = new Date(); + let currentMonth = currentDate.getMonth(); + let currentYear = currentDate.getFullYear(); + let selectedDate = null; // Track selected date + + const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + + // Fetch available dates from the API + async function fetchAvailability() { + try { + const response = await fetch(`/api/availability/${uid}`); + if (response.status === 404) { + // UID doesn't exist, show error page + mainContent.style.display = 'none'; + uidError.style.display = 'block'; + return null; + } + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const availability = await response.json(); + console.log('Fetched availability for UID:', uid, availability); + return availability; + } catch (error) { + console.error('Error fetching availability:', error); + return {}; + } + } + + // Render the calendar with available dates highlighted and automatically select/display the current date + async function renderCalendar(month, year) { + const availability = await fetchAvailability(); + if (availability === null) return; // UID doesn't exist + + calendarDates.innerHTML = ''; + monthYear.textContent = `${months[month]} ${year}`; + + const firstDay = new Date(year, month, 1).getDay(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const today = new Date(); + + // Create blank cells for days before the first day of the month + for (let i = 0; i < firstDay; i++) { + const blank = document.createElement('div'); + blank.classList.add('date-item', 'p-2', 'rounded-pill', 'text-center', 'text-muted'); + calendarDates.appendChild(blank); + } + + // Populate the calendar with days + for (let i = 1; i <= daysInMonth; i++) { + const day = document.createElement('div'); + day.classList.add('date-item', 'p-2', 'rounded-pill', 'text-center'); + day.textContent = i; + const date = new Date(year, month, i); + const dateStr = date.toISOString().split('T')[0]; + + // Start of today (midnight) + const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()); + + // Check if date is in the past + const isPastDate = date < startOfToday; + + if (date.toDateString() === today.toDateString()) { + day.classList.add('current'); + } + if (availability[dateStr]) { + day.classList.add('available'); + } + if (isPastDate) { + day.classList.add('disabled'); + } else { + day.addEventListener('click', () => selectDate(date, firstDay)); + } + calendarDates.appendChild(day); + } + + // Auto-select today if we're in the current month/year + if (today.getMonth() === month && today.getFullYear() === year) { + await selectDate(today, firstDay); + // Also update time slots directly since we're on today's date + await updateTimeSlots(today); + } + } + + // Handle date selection + async function selectDate(date, firstDay) { + // Check if the date is in the past + const today = new Date(); + const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()); + if (date < startOfToday) { + console.warn('Attempted to select a past date'); + return; + } + + selectedDate = date; + const dateItems = document.querySelectorAll('#calendarDates .date-item'); + dateItems.forEach(item => item.classList.remove('selected')); // Ensure only one date is selected + // Find the exact date item for the clicked or auto-selected date + const selectedDay = Array.from(dateItems).find(item => { + const dayText = item.textContent; + const itemDate = new Date(currentYear, currentMonth, parseInt(dayText)); + return itemDate.toDateString() === date.toDateString(); + }); + if (selectedDay) selectedDay.classList.add('selected'); + await updateTimeSlots(date); // Update time slots based on selected date, ensuring async completion + } + + // Update time slots based on selected date + async function updateTimeSlots(date) { + const availability = await fetchAvailability(); + const dateStr = date.toISOString().split('T')[0]; + console.log('Looking for date:', dateStr); + console.log('Available dates:', Object.keys(availability)); + let times = []; + if (availability[dateStr]) { + console.log('Found times for date:', availability[dateStr]); + // Ensure availability[dateStr] is a string or array before processing + if (typeof availability[dateStr] === 'string') { + times = availability[dateStr].split(',').map(t => t.trim()); + } else if (Array.isArray(availability[dateStr])) { + times = availability[dateStr].map(t => t.trim()); + } else { + console.warn(`Unexpected data format for date ${dateStr}:`, availability[dateStr]); + } + + // Filter out past times if it's today + const today = new Date(); + const isToday = date.toDateString() === today.toDateString(); + console.log('Time filtering - Today:', today.toDateString()); + console.log('Time filtering - Selected:', date.toDateString()); + console.log('Is today?', isToday); + if (isToday) { + const currentMinutes = today.getHours() * 60 + today.getMinutes(); + console.log('Current minutes:', currentMinutes); + console.log('Times before filtering:', times); + times = times.filter(time => { + const timeMin = timeToMinutes(time); + console.log(`Time ${time} = ${timeMin} minutes`); + return timeMin > currentMinutes; + }); + console.log('Times after filtering:', times); + } + + // Sort times chronologically + times.sort((a, b) => timeToMinutes(a) - timeToMinutes(b)); + } + + // Clear existing time slots or message + timeSlotsContainer.innerHTML = ''; + + if (times.length === 0) { + // Display "No availability on selected date." if no times are available + const noAvailabilityMsg = document.createElement('p'); + noAvailabilityMsg.classList.add('text-muted', 'text-center', 'fw-bold', 'fs-5', 'py-3'); + noAvailabilityMsg.textContent = 'No availability on selected date.'; + timeSlotsContainer.appendChild(noAvailabilityMsg); + } else { + // Populate time slots dynamically if times are available + times.forEach(time => { + const button = document.createElement('button'); + button.classList.add('time-slot', 'btn', 'rounded-pill', 'w-100', 'mb-2'); + button.classList.add('btn-outline-primary', 'available'); + button.textContent = time; + button.addEventListener('click', () => { + const allTimeSlots = timeSlotsContainer.querySelectorAll('.time-slot'); + allTimeSlots.forEach(s => s.classList.remove('selected')); + button.classList.add('selected'); + + // Format date as MM/DD/YY + const [year, month, day] = dateStr.split('-'); + const formattedDate = `${month}/${day}/${year.slice(2)}`; + + // Show booking modal + document.getElementById('selectedTime').value = time; + document.getElementById('selectedDate').value = dateStr; + document.getElementById('bookingModalLabel').textContent = `Confirm Appointment for ${formattedDate} at ${time} EST`; + + // Clear previous form data + document.getElementById('bookingForm').reset(); + + // Show modal + const bookingModal = new bootstrap.Modal(document.getElementById('bookingModal')); + bookingModal.show(); + }); + timeSlotsContainer.appendChild(button); + }); + } + } + + // Navigate to previous month + prevMonthBtn.addEventListener('click', () => { + currentMonth--; + if (currentMonth < 0) { + currentMonth = 11; + currentYear--; + } + renderCalendar(currentMonth, currentYear); + }); + + // Navigate to next month + nextMonthBtn.addEventListener('click', () => { + currentMonth++; + if (currentMonth > 11) { + currentMonth = 0; + currentYear++; + } + renderCalendar(currentMonth, currentYear); + }); + + // Update current time in Eastern Time (US & Canada) + function updateCurrentTime() { + const options = { + timeZone: 'America/New_York', + hour: '2-digit', + minute: '2-digit', + hour12: true + }; + const time = new Date().toLocaleTimeString('en-US', options); + currentTimeElement.textContent = time; + } + + // Update time every second + setInterval(updateCurrentTime, 1000); + updateCurrentTime(); // Initial call + + // Form validation and submission + const form = document.getElementById('bookingForm'); + const submitBtn = document.getElementById('submitBooking'); + + // Email validation function + function isValidEmail(email) { + const re = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + return re.test(email); + } + + // Phone validation function (optional field) + function isValidPhone(phone) { + if (!phone) return true; // Optional field + const re = /^[0-9]{3}-[0-9]{3}-[0-9]{4}$/; + return re.test(phone); + } + + // Format phone number as user types + document.getElementById('phone').addEventListener('input', (e) => { + let value = e.target.value.replace(/\D/g, ''); // Remove non-digits + if (value.length >= 3) { + value = value.slice(0, 3) + '-' + value.slice(3); + if (value.length >= 7) { + value = value.slice(0, 7) + '-' + value.slice(7); + } + } + e.target.value = value.slice(0, 12); // Limit to XXX-XXX-XXXX + }); + + // Validate fields on blur + document.getElementById('name').addEventListener('blur', (e) => { + if (!e.target.value.trim()) { + e.target.classList.add('is-invalid'); + } else { + e.target.classList.remove('is-invalid'); + } + }); + + document.getElementById('email').addEventListener('blur', (e) => { + const email = e.target.value.trim(); + if (!email || !isValidEmail(email)) { + e.target.classList.add('is-invalid'); + } else { + e.target.classList.remove('is-invalid'); + } + }); + + document.getElementById('phone').addEventListener('blur', (e) => { + const phone = e.target.value.trim(); + if (phone && !isValidPhone(phone)) { + e.target.classList.add('is-invalid'); + } else { + e.target.classList.remove('is-invalid'); + } + }); + + submitBtn.addEventListener('click', async (e) => { + e.preventDefault(); + + const name = document.getElementById('name').value.trim(); + const email = document.getElementById('email').value.trim(); + const phone = document.getElementById('phone').value.trim(); + const notes = document.getElementById('notes').value.trim(); + const selectedTime = document.getElementById('selectedTime').value; + const selectedDate = document.getElementById('selectedDate').value; + + // Clear previous validation state + form.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid')); + + // Validate required fields + let hasErrors = false; + + if (!name) { + document.getElementById('name').classList.add('is-invalid'); + hasErrors = true; + } + + if (!email || !isValidEmail(email)) { + document.getElementById('email').classList.add('is-invalid'); + hasErrors = true; + } + + // Validate phone if provided + if (phone && !isValidPhone(phone)) { + document.getElementById('phone').classList.add('is-invalid'); + hasErrors = true; + } + + if (hasErrors) { + return; + } + + // Prepare booking data + const bookingData = { + name, + email, + phone: phone || null, + notes: notes || null, + date: selectedDate, + time: selectedTime + }; + + try { + const response = await fetch('/api/book', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(bookingData) + }); + + if (!response.ok) { + throw new Error('Booking failed'); + } + + // Close modal and show success message + const modal = bootstrap.Modal.getInstance(document.getElementById('bookingModal')); + modal.hide(); + alert('Appointment scheduled successfully!'); + + // Refresh the calendar + await renderCalendar(currentMonth, currentYear); + } catch (error) { + console.error('Error booking appointment:', error); + alert('Failed to schedule appointment. Please try again.'); + } + }); + + // Initial render and setup + await renderCalendar(currentMonth, currentYear); + + // Force an immediate update of time slots for today's date + const today = new Date(); + selectedDate = today; // Ensure selectedDate is set + await updateTimeSlots(today); + + // Double-check that time slots are updated + if (!timeSlotsContainer.hasChildNodes()) { + const noAvailabilityMsg = document.createElement('p'); + noAvailabilityMsg.classList.add('text-muted', 'text-center', 'fw-bold', 'fs-5', 'py-3'); + noAvailabilityMsg.textContent = 'No availability on selected date.'; + timeSlotsContainer.appendChild(noAvailabilityMsg); + } +}); \ No newline at end of file diff --git a/public/script.js b/public/script.js new file mode 100644 index 0000000..56fb91d --- /dev/null +++ b/public/script.js @@ -0,0 +1,245 @@ +// public/public.js (for index.html) +document.addEventListener('DOMContentLoaded', async () => { + const calendarDates = document.getElementById('calendarDates'); + const monthYear = document.getElementById('monthYear'); + const prevMonthBtn = document.getElementById('prevMonth'); + const nextMonthBtn = document.getElementById('nextMonth'); + const currentTimeElement = document.getElementById('currentTime'); + const timeSlotsContainer = document.getElementById('timeSlots'); + + // Check if navigation buttons exist before adding event listeners + if (!prevMonthBtn || !nextMonthBtn) { + console.error('Navigation buttons (prevMonth or nextMonth) not found in the DOM'); + return; + } + + let currentDate = new Date(); + let currentMonth = currentDate.getMonth(); + let currentYear = currentDate.getFullYear(); + let selectedDate = null; // Track selected date + + const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + + // Fetch available dates from the API + async function fetchAvailability() { + try { + const response = await fetch('/api/availability'); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const availability = await response.json(); + return availability; + } catch (error) { + console.error('Error fetching availability:', error); + return {}; + } + } + + // Render the calendar with available dates highlighted and automatically select/display the current date + async function renderCalendar(month, year) { + // Fetch availability first to ensure data is ready + const availability = await fetchAvailability(); + + calendarDates.innerHTML = ''; + monthYear.textContent = `${months[month]} ${year}`; + + const firstDay = new Date(year, month, 1).getDay(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + const today = new Date(); + + // Start of today (midnight) + const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()); + + // Create blank cells for days before the first day of the month + for (let i = 0; i < firstDay; i++) { + const blank = document.createElement('div'); + blank.classList.add('date-item', 'p-2', 'rounded-pill', 'text-center', 'text-muted'); + calendarDates.appendChild(blank); + } + + // Populate the calendar with days + for (let i = 1; i <= daysInMonth; i++) { + const day = document.createElement('div'); + day.classList.add('date-item', 'p-2', 'rounded-pill', 'text-center'); + day.textContent = i; + const date = new Date(year, month, i); + const dateStr = date.toISOString().split('T')[0]; + + // Check if date is in the past + const isPastDate = date < startOfToday; + + if (date.toDateString() === today.toDateString()) { + day.classList.add('current'); + } + if (availability[dateStr]) { + day.classList.add('available'); + } + if (isPastDate) { + day.classList.add('text-muted'); + day.style.cursor = 'not-allowed'; + } else { + day.addEventListener('click', () => selectDate(date, firstDay)); + day.style.cursor = 'pointer'; + } + calendarDates.appendChild(day); + } + + // Only try to select today if we're in the current month and year + if (today.getMonth() === month && today.getFullYear() === year) { + await selectDate(today, firstDay); // Use await to ensure selection and display complete + } + } + + // Handle date selection + async function selectDate(date, firstDay) { + // Check if the date is in the past + const today = new Date(); + const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()); + if (date < startOfToday) { + console.warn('Attempted to select a past date'); + return; + } + + selectedDate = date; + const dateItems = document.querySelectorAll('#calendarDates .date-item'); + dateItems.forEach(item => item.classList.remove('selected')); // Ensure only one date is selected + // Find the exact date item for the clicked or auto-selected date + const selectedDay = Array.from(dateItems).find(item => { + const dayText = item.textContent; + const itemDate = new Date(currentYear, currentMonth, parseInt(dayText)); + return itemDate.toDateString() === date.toDateString(); + }); + if (selectedDay) selectedDay.classList.add('selected'); + await updateTimeSlots(date); // Update time slots based on selected date, ensuring async completion + } + + // Helper function to convert time string to minutes for sorting + function timeToMinutes(timeStr) { + console.log('Converting time:', timeStr); + const match = timeStr.match(/([0-9]+)(?::([0-9]+))?(am|pm)/i); + if (!match) { + console.warn('No match for time:', timeStr); + return 0; + } + + let [_, hours, minutes, period] = match; + hours = parseInt(hours); + minutes = minutes ? parseInt(minutes) : 0; + period = period.toLowerCase(); + + if (period === 'pm' && hours !== 12) hours += 12; + if (period === 'am' && hours === 12) hours = 0; + + const totalMinutes = hours * 60 + minutes; + console.log(`${timeStr} -> ${totalMinutes} minutes`); + return totalMinutes; + } + + // Update time slots based on selected date + async function updateTimeSlots(date) { + const availability = await fetchAvailability(); + const dateStr = date.toISOString().split('T')[0]; + let times = []; + if (availability[dateStr]) { + // Ensure availability[dateStr] is a string or array before processing + if (typeof availability[dateStr] === 'string') { + times = availability[dateStr].split(',').map(t => t.trim()); + } else if (Array.isArray(availability[dateStr])) { + times = availability[dateStr].map(t => t.trim()); + } else { + console.warn(`Unexpected data format for date ${dateStr}:`, availability[dateStr]); + } + } + + // Clear existing time slots or message + timeSlotsContainer.innerHTML = ''; + + // Get current time in minutes for comparison + const now = new Date(); + const currentMinutes = now.getHours() * 60 + now.getMinutes(); + const isToday = date.toDateString() === now.toDateString(); + + // Filter out past times if it's today + if (isToday) { + times = times.filter(time => timeToMinutes(time) > currentMinutes); + } + + // Sort remaining times chronologically + times.sort((a, b) => timeToMinutes(a) - timeToMinutes(b)); + + if (times.length === 0) { + // Display appropriate message based on context + const noAvailabilityMsg = document.createElement('p'); + noAvailabilityMsg.classList.add('text-muted', 'text-center', 'fw-bold', 'fs-5', 'py-3'); + + let message; + if (isToday) { + if (currentMinutes > timeToMinutes('5:00pm')) { + message = 'No more availability today.'; + } else { + message = 'No available time slots for today.'; + } + } else { + message = 'No availability on selected date.'; + } + + noAvailabilityMsg.textContent = message; + timeSlotsContainer.appendChild(noAvailabilityMsg); + } else { + // Populate time slots dynamically + times.forEach(time => { + const button = document.createElement('button'); + button.classList.add('time-slot', 'btn', 'rounded-pill', 'w-100', 'mb-2'); + button.classList.add('btn-outline-primary', 'available'); + button.textContent = time; + button.addEventListener('click', () => { + if (button.classList.contains('available') && selectedDate) { + const allTimeSlots = timeSlotsContainer.querySelectorAll('.time-slot'); + allTimeSlots.forEach(s => s.classList.remove('selected')); + button.classList.add('selected'); + } + }); + timeSlotsContainer.appendChild(button); + }); + } + } + + // Navigate to previous month + prevMonthBtn.addEventListener('click', () => { + currentMonth--; + if (currentMonth < 0) { + currentMonth = 11; + currentYear--; + } + renderCalendar(currentMonth, currentYear); + }); + + // Navigate to next month + nextMonthBtn.addEventListener('click', () => { + currentMonth++; + if (currentMonth > 11) { + currentMonth = 0; + currentYear++; + } + renderCalendar(currentMonth, currentYear); + }); + + // Update current time in Eastern Time (US & Canada) + function updateCurrentTime() { + const options = { + timeZone: 'America/New_York', + hour: '2-digit', + minute: '2-digit', + hour12: true + }; + const time = new Date().toLocaleTimeString('en-US', options); + currentTimeElement.textContent = time; + } + + // Update time every second + setInterval(updateCurrentTime, 1000); + updateCurrentTime(); // Initial call + + // Initial render (set to current date, automatically selecting and displaying it) + await renderCalendar(currentMonth, currentYear); // Use await to ensure the initial render completes +}); \ No newline at end of file diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..8f7293f --- /dev/null +++ b/public/styles.css @@ -0,0 +1,234 @@ +/* Custom styles to modernize and align with Calendly-inspired design */ +:root { + --primary-color: #007bff; /* Blue from Calendly */ + --secondary-color: #6c757d; /* Gray for muted elements */ + --light-bg: #f8f9fa; /* Light background for modern look */ + --shadow-color: rgba(0, 0, 0, 0.1); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Arial', sans-serif; /* Matches Calendly font */ +} + +.container-fluid { + max-width: 1000px; + margin: 20px auto; + background-color: #fff; + border-radius: 6px; + box-shadow: 0 2px 4px var(--shadow-color); + overflow: hidden; +} + +.sidebar { + width: 30%; + padding: 20px; + border-right: 1px solid #e1e8ed; + text-align: center; + background-color: #fff; +} + +.profile-pic { + width: 80px; + height: 80px; + object-fit: cover; + border-radius: 50%; + margin-bottom: 10px; + border: 2px solid #fff; + box-shadow: 0 2px 4px var(--shadow-color); +} + +.sidebar h2 { + font-size: 1.25rem; + color: #333; + margin-bottom: 5px; + font-weight: 600; +} + +.sidebar h3 { + font-size: 1rem; + color: #666; + margin-bottom: 5px; +} + +.duration { + font-size: 0.875rem; + color: #666; +} + +.main-content { + width: 70%; + padding: 20px; +} + +.calendar-header h1 { + font-size: 1.5rem; + color: #333; + font-weight: 700; + margin-bottom: 10px; +} + +.month-nav { + display: flex; + justify-content: space-between; + font-size: 1rem; + color: #333; + padding: 0.5rem 0; + border-bottom: 1px solid #e1e8ed; +} + +.month-nav button { + color: var(--secondary-color); + transition: color 0.3s ease; +} + +.month-nav button:hover { + color: var(--primary-color); +} + +.days-of-week, .dates { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 5px; + text-align: center; +} + +.days-of-week span { + font-weight: bold; + color: #666; + padding: 5px; +} + +.dates .date-item { + padding: 10px; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.3s ease, transform 0.2s ease; +} + +.dates .date-item.disabled { + cursor: not-allowed; + color: var(--secondary-color); + opacity: 0.5; +} + +.dates .date-item:hover { + background-color: #f0f8ff; + transform: scale(1.05); +} + +.dates .date-item.available { + background-color: #e9f2ff; + border: 2px solid var(--primary-color); + font-weight: 600; +} + +.dates .date-item.selected { + background-color: #0d6efd !important; + color: #fff !important; + border-radius: 6px; + font-weight: 600; + border: none !important; +} + +.dates .date-item.current { + background-color: #e9f2ff; + border: 2px solid var(--primary-color); + font-weight: 600; +} + +.time-slot { + font-size: 1rem; + padding: 0.75rem 1.5rem; + transition: background-color 0.3s ease, border-color 0.3s ease, transform 0.2s ease; +} + +.time-slot.available { + background-color: #e9f2ff; + border-color: var(--primary-color); + color: var(--primary-color); +} + +.time-slots .text-muted { + color: #6c757d; /* Muted text color, matching Calendly */ + font-size: 1rem; /* Slightly larger font, e.g., 20px instead of 16px */ + font-weight: bold; /* Bold text */ + padding: 1rem 0; + text-align: center; +} + +.time-slot.available:hover, .time-slot:hover { + background-color: var(--primary-color); + color: #fff; + transform: scale(1.05); +} + +.timezone { + font-size: 0.875rem; + color: #666; +} + +.powered-by { + font-size: 0.75rem; + color: #666; + text-align: right; + position: relative; + margin-top: 2rem; + opacity: 0.8; + transition: opacity 0.3s ease; + z-index: 1; +} + +.powered-by a { + color: inherit; + text-decoration: none; + cursor: pointer; +} + +.powered-by:hover { + opacity: 1; +} + +/* Admin-specific styles */ +#admin .calendar { + margin-bottom: 20px; +} + +#admin .form-control, #admin .form-select { + border-radius: 6px; +} + +#admin .btn-primary, #admin .btn-danger { + border-radius: 4px; +} + +#availabilityList .list-group-item { + border-radius: 4px; + margin-bottom: 5px; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .container-fluid { + margin: 10px; + border-radius: 6px; + } + + .sidebar, .main-content { + width: 100%; + } + + .sidebar { + border-right: none; + border-bottom: 1px solid #e1e8ed; + } + + #admin .row { + flex-direction: column; + } + #admin .col-md-6 { + width: 100%; + } +} \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..002598c --- /dev/null +++ b/server.js @@ -0,0 +1,419 @@ +const express = require('express'); +const path = require('path'); +const sqlite3 = require('sqlite3').verbose(); + +const app = express(); +const port = 3000; + +// Serve static files from the 'public' directory +app.use(express.static(path.join(__dirname, 'public'))); + +// Serve HTML files from the 'views' directory +app.get('/', (req, res) => { + res.sendFile(path.join(__dirname, 'views', 'index.html')); +}); + +app.get('/admin', (req, res) => { + res.sendFile(path.join(__dirname, 'views', 'admin.html')); +}); + +// Middleware to parse JSON bodies +app.use(express.json()); + +// Helper function to create database tables +const createDatabaseTables = (callback) => { + db.serialize(() => { + // Enable foreign key support + db.run('PRAGMA foreign_keys = ON', (err) => { + if (err) { + callback(new Error(`Failed to enable foreign keys: ${err.message}`)); + return; + } + + // Create uids table + db.run(`CREATE TABLE IF NOT EXISTS uids ( + uid TEXT PRIMARY KEY, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`, (err) => { + if (err) { + callback(new Error(`Failed to create uids table: ${err.message}`)); + return; + } + + // Create availability table + db.run(`CREATE TABLE IF NOT EXISTS availability ( + uid TEXT, + date TEXT, + times TEXT, + PRIMARY KEY (uid, date), + FOREIGN KEY (uid) REFERENCES uids(uid) ON DELETE CASCADE + )`, (err) => { + if (err) { + callback(new Error(`Failed to create availability table: ${err.message}`)); + return; + } + callback(null); + }); + }); + }); + }); +}; + +// Helper function to initialize database +const initializeDatabase = () => { + // Ensure db directory exists + const fs = require('fs'); + const dbDir = 'db'; + if (!fs.existsSync(dbDir)) { + fs.mkdirSync(dbDir); + } + + // Connect to SQLite database + db = new sqlite3.Database('db/freesched.db', (err) => { + if (err) { + console.error('Could not connect to database:', err.message); + return; + } + console.log('Connected to SQLite database'); + + // Create tables + createDatabaseTables((err) => { + if (err) { + console.error('Error creating database tables:', err.message); + return; + } + console.log('Database tables created successfully'); + }); + }); +}; + +// Initialize database +let db; +initializeDatabase(); + +// Admin view - must be defined before the catch-all UID route +app.get('/admin', (req, res) => { + res.sendFile(path.join(__dirname, 'views', 'admin.html')); +}); + +// Public view - handle both root and UID-based routes +app.get(['/', '/:uid'], (req, res) => { + // Only validate UID if it's provided (not root path) + if (req.params.uid) { + // Validate UID format + if (!/^[a-z0-9-]+$/.test(req.params.uid)) { + res.sendFile(path.join(__dirname, 'views', 'index.html')); + return; + } + } + res.sendFile(path.join(__dirname, 'views', 'index.html')); +}); + +// Helper function to convert time string to minutes for sorting +function timeToMinutes(timeStr) { + const match = timeStr.match(/([0-9]+)(?::([0-9]+))?(am|pm)/i); + if (!match) return 0; + + let [_, hours, minutes, period] = match; + hours = parseInt(hours); + minutes = minutes ? parseInt(minutes) : 0; + period = period.toLowerCase(); + + if (period === 'pm' && hours !== 12) hours += 12; + if (period === 'am' && hours === 12) hours = 0; + + return hours * 60 + minutes; +} + +// API endpoint to get available dates and times for a specific UID +app.get('/api/availability/:uid', (req, res) => { + const { uid } = req.params; + + // Verify UID exists + db.get('SELECT uid FROM uids WHERE uid = ?', [uid], (err, row) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + if (!row) { + res.status(404).json({ error: 'UID not found' }); + return; + } + + // Get availability for this UID + db.all('SELECT date, times FROM availability WHERE uid = ?', [uid], (err, rows) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + const availability = {}; + rows.forEach(row => { + // Handle case where times might be null or undefined + if (row.times) { + // Split, trim, and sort times chronologically + const times = row.times.split(',').map(t => t.trim()); + times.sort((a, b) => timeToMinutes(a) - timeToMinutes(b)); + availability[row.date] = times; + } else { + availability[row.date] = []; + } + }); + res.json(availability); + }); + }); +}); + +// API endpoint to add/update availability for a specific UID +app.post('/api/availability/:uid', (req, res) => { + const { uid } = req.params; + const { date, times } = req.body; + + if (!date) { + res.status(400).json({ error: 'Date is required' }); + return; + } + + // Verify UID exists + db.get('SELECT uid FROM uids WHERE uid = ?', [uid], (err, row) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + if (!row) { + res.status(404).json({ error: 'UID not found' }); + return; + } + + if (times.length === 0) { + // Delete the entry if no times remain + db.run('DELETE FROM availability WHERE uid = ? AND date = ?', [uid, date], (err) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + res.json({ message: 'Availability removed successfully' }); + }); + } else { + // Sort times before storing + const sortedTimes = [...times].sort((a, b) => timeToMinutes(a) - timeToMinutes(b)); + + // Insert or replace with the sorted times + db.run('INSERT OR REPLACE INTO availability (uid, date, times) VALUES (?, ?, ?)', + [uid, date, sortedTimes.join(',')], + (err) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + res.json({ message: 'Availability updated successfully' }); + } + ); + } + }); +}); + + + +// API endpoint to get all UIDs +app.get('/api/uids', (req, res) => { + createDatabaseTables((err) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + // Get all UIDs + db.all('SELECT uid, created_at FROM uids ORDER BY created_at DESC', (err, rows) => { + if (err) { + res.status(500).json({ error: `Failed to fetch UIDs: ${err.message}` }); + return; + } + res.json(rows || []); + }); + }); +}); + +// API endpoint to create a new UID +app.post('/api/uids', (req, res) => { + const { uid } = req.body; + + // Validate UID format (lowercase letters, numbers, and hyphens only) + if (!uid || !/^[a-z0-9-]+$/.test(uid)) { + res.status(400).json({ error: 'Invalid UID format. Use only lowercase letters, numbers, and hyphens.' }); + return; + } + + createDatabaseTables((err) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + + db.run('INSERT INTO uids (uid) VALUES (?)', [uid], function(err) { + if (err) { + if (err.code === 'SQLITE_CONSTRAINT') { + res.status(409).json({ error: 'UID already exists' }); + } else { + res.status(500).json({ error: `Failed to insert UID: ${err.message}` }); + } + return; + } + res.status(201).json({ uid, created_at: new Date().toISOString() }); + }); + }); +}); + +// API endpoint to delete a UID and its availability +app.delete('/api/uids/:uid', (req, res) => { + const { uid } = req.params; + + // Begin transaction + db.serialize(() => { + db.run('BEGIN TRANSACTION'); + + // Delete from availability first due to foreign key constraint + db.run('DELETE FROM availability WHERE uid = ?', [uid], (err) => { + if (err) { + db.run('ROLLBACK'); + res.status(500).json({ error: err.message }); + return; + } + + // Then delete from uids + db.run('DELETE FROM uids WHERE uid = ?', [uid], function(err) { + if (err) { + db.run('ROLLBACK'); + res.status(500).json({ error: err.message }); + return; + } + if (this.changes === 0) { + db.run('ROLLBACK'); + res.status(404).json({ error: 'UID not found' }); + return; + } + + // Commit transaction + db.run('COMMIT', (err) => { + if (err) { + db.run('ROLLBACK'); + res.status(500).json({ error: err.message }); + return; + } + res.json({ message: 'UID deleted successfully' }); + }); + }); + }); + }); +}); + +// API endpoint to flush (delete) all availability entries for a specific UID +app.delete('/api/availability/:uid/flush', (req, res) => { + const { uid } = req.params; + + // Verify UID exists + db.get('SELECT uid FROM uids WHERE uid = ?', [uid], (err, row) => { + if (err) { + res.status(500).json({ error: err.message }); + return; + } + if (!row) { + res.status(404).json({ error: 'UID not found' }); + return; + } + + // Delete all availability for this UID + db.run('DELETE FROM availability WHERE uid = ?', [uid], (err) => { + if (err) { + console.error('Error deleting availability:', err); + res.status(500).json({ error: err.message }); + return; + } + console.log(`All availability entries for UID ${uid} deleted successfully`); + res.json({ message: `All availability entries for UID ${uid} deleted successfully` }); + }); + }); +}); + +// API endpoint to reset the entire database +app.post('/api/reset', async (req, res) => { + const fs = require('fs').promises; + const path = require('path'); + const dbDir = 'db'; + const dbPath = path.join(dbDir, 'freesched.db'); + + try { + // Close current database connection + await new Promise((resolve, reject) => { + if (!db) { + resolve(); + return; + } + db.close((err) => { + if (err) reject(err); + else resolve(); + }); + }); + + // Ensure db directory exists + try { + await fs.access(dbDir); + } catch { + await fs.mkdir(dbDir); + } + + // Delete existing database if it exists + try { + await fs.unlink(dbPath); + } catch (err) { + if (err.code !== 'ENOENT') throw err; // Ignore if file doesn't exist + } + + // Initialize fresh database + initializeDatabase(); + + // Wait for database to be ready + await new Promise((resolve, reject) => { + const checkDb = () => { + if (!db) { + setTimeout(checkDb, 100); // Check every 100ms + return; + } + + // Test database connection + db.get('SELECT 1', (err) => { + if (err) { + setTimeout(checkDb, 100); + } else { + resolve(); + } + }); + }; + checkDb(); + + // Set a timeout of 5 seconds + setTimeout(() => { + reject(new Error('Database initialization timeout')); + }, 5000); + }); + + res.json({ message: 'Database completely wiped and recreated successfully' }); + } catch (error) { + console.error('Error in flush-global:', error); + + // Try to reinitialize database + try { + initializeDatabase(); + } catch (reinitError) { + console.error('Failed to reinitialize database:', reinitError); + } + + // Ensure we haven't already sent a response + if (!res.headersSent) { + res.status(500).json({ error: error.message }); + } + } +}); + +app.listen(port, () => { + console.log(`FreeSched app listening at http://localhost:${port}`); +}); \ No newline at end of file diff --git a/views/admin.html b/views/admin.html new file mode 100644 index 0000000..ef56e66 --- /dev/null +++ b/views/admin.html @@ -0,0 +1,110 @@ + + +
+ + +The actions in this section can cause irreversible changes to the database. Please proceed with caution.
+ +Eastern Time - US & Canada ()
+ +